﻿namespace SuperModel.Generation
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Reflection;
    using System.Reflection.Emit;

    /// <summary>
    /// The property implementer
    /// </summary>
    internal class PropertyImplementer
    {
        /// <summary>
        /// The attribute transmitter
        /// </summary>
        private readonly AttributeTransmitter attributeTransmitter = new AttributeTransmitter();

        /// <summary>
        /// The property set and property get methods require a special set of attributes.
        /// </summary>
        private const MethodAttributes PropertyAccessorAttributes = MethodAttributes.Public |
                                                                    MethodAttributes.HideBySig |
                                                                    MethodAttributes.NewSlot |
                                                                    MethodAttributes.SpecialName |
                                                                    MethodAttributes.Virtual;

        /// <summary>
        /// Implement A Data-Change Notifying Property
        /// </summary>
        /// <param name="newType">The type being implemented</param>
        /// <param name="property">The property being implemented</param>
        /// <param name="fields">The fields on the type</param>
        public void ImplementDataChangingProperty(TypeBuilder newType, PropertyInfo property, IList<FieldBuilder> fields)
        {
            // Add a Field to the Type for the Property
            FieldBuilder newField = newType.DefineField("m_" + property.Name, property.PropertyType, FieldAttributes.Private);
            fields.Add(newField);

            // Define the New Property
            PropertyBuilder newProperty = newType.DefineProperty(property.Name, PropertyAttributes.None, property.PropertyType, null);
            ImplementSimplePropertyGet(property, newType, newProperty, newField);
            ImplementSimplePropertySet(property, newType, newProperty, newField);
            attributeTransmitter.ApplyAttributesToProperty(property, newProperty);
        }

        /// <summary>
        /// Implements a simple property setter.
        /// </summary>
        /// <param name="property">
        /// The property.
        /// </param>
        /// <param name="newType">
        /// The new type.
        /// </param>
        /// <param name="propertyBuilder">
        /// The property builder.
        /// </param>
        /// <param name="newField">
        /// The new field.
        /// </param>
        private static void ImplementSimplePropertySet(PropertyInfo property, TypeBuilder newType, PropertyBuilder propertyBuilder, FieldBuilder newField)
        {
            // Define the "set" accessor method for CustomerName.
            MethodBuilder setMethodBuilder = newType.DefineMethod("set_" + property.Name, PropertyAccessorAttributes, null, new[] { property.PropertyType });

            ILGenerator setMethodImpl = setMethodBuilder.GetILGenerator();
            const BindingFlags BindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;

            MethodInfo callBase = newType.BaseType.GetMethod("NotifyByName", BindingFlags);
            Debug.Assert(callBase != null, "The method call to update must not be null");

            SetFieldToValue(setMethodImpl, newField);
            CallNotifyInBase(property.Name, callBase, setMethodImpl);
            setMethodImpl.Emit(OpCodes.Ret);
            propertyBuilder.SetSetMethod(setMethodBuilder);
        }

        /// <summary>
        /// Sets a filed to a new value and calls the base method
        /// </summary>
        /// <param name="propertyName">
        /// The property name.
        /// </param>
        /// <param name="callNotify">
        /// The call notify.
        /// </param>
        /// <param name="gen">
        /// The il gen.
        /// </param>
        private static void CallNotifyInBase(string propertyName, MethodInfo callNotify, ILGenerator gen)
        {
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldstr, propertyName);
            gen.Emit(OpCodes.Call, callNotify);
        }

        /// <summary>
        /// Sets the field to the Value
        /// </summary>
        /// <param name="gen">
        /// The il gen.
        /// </param>
        /// <param name="newField">
        /// The new field.
        /// </param>
        private static void SetFieldToValue(ILGenerator gen, FieldInfo newField)
        {
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Stfld, newField);
        }

        /// <summary>
        /// Implement a basic property getter
        /// </summary>
        /// <param name="property">
        /// The property definition.
        /// </param>
        /// <param name="newType">
        /// The new Type being generated.
        /// </param>
        /// <param name="propertyBuilder">
        /// The new Type's property builder for this property.
        /// </param>
        /// <param name="newField">
        /// The property's backing field.
        /// </param>
        private static void ImplementSimplePropertyGet(PropertyInfo property, TypeBuilder newType, PropertyBuilder propertyBuilder, FieldBuilder newField)
        {
            MethodBuilder getMethodBuilder = newType.DefineMethod("get_" + property.Name, PropertyAccessorAttributes, property.PropertyType, Type.EmptyTypes);

            // Define the "get" method implementation
            ILGenerator getMethodImpl = getMethodBuilder.GetILGenerator();
            getMethodImpl.Emit(OpCodes.Ldarg_0);
            getMethodImpl.Emit(OpCodes.Ldfld, newField);
            getMethodImpl.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getMethodBuilder);
        }
    }
}